# -*- coding:utf-8 -*-
"""
KISIN is Korea ISIN (International Securities Identifying Number) code parser.
Currently, sub-code parsing is possible for stock, ETF, ELW, future, options. 

KISIN provides the functions:
    * parse KISIN code to a dictionary containing security specifications
    * convert KISIN and simplified KISIN
    
"""

from string import ascii_uppercase

# security category code
CATEGORY = { '1': "government bond",
             '2': "municipal bond",
             '3': "government-agency bond",
             '4': "future and option",
             '5': "fund",
             '6': "hybrid",
             '7': "stock",
             '8': "deposit receipt",
             '9': "warrant",
             } 

# future and option type code
FUTUREOPTION_CATEGORY = { '1': "future",
                          '2': "call",
                          '3': "put",
                          '4': "spread",
                          '5': "call on future",
                          '6': "put on future",
                          '7': "flex future",
                          } 

# future and option underlying code
FUTUREOPTION_UNDERLYING = { '01' : 'kospi200 index' }

# future and option maturity year code
FUTUREOPTION_MAT_YEAR = { '6': 1996,
                          '7': 1997,
                          '8': 1998,
                          '9': 1999,
                          '0': 2000,
                          '1': 2001,
                          '2': 2002,
                          '3': 2003,
                          '4': 2004,
                          '5': 2005,
                          'A': 2006,
                          'B': 2007,
                          'C': 2008,
                          'D': 2009,
                          'E': 2010,
                          'F': 2011,
                          'G': 2012,
                          'H': 2013,
                          'J': 2014,
                          'K': 2015,
                          'L': 2016,
                          'M': 2017,
                          'N': 2018,
                          'P': 2019,
                          'Q': 2020,
                          'R': 2021,
                          'S': 2022,
                          'T': 2023,
                          'V': 2024,
                          'W': 2025,
                          #'6': 2026,
                          #'7': 2027,
                          #'8': 2028,
                          #'9': 2029,
                          #'0': 2030,
                          #'1': 2031,
                          #'2': 2032,
                          #'3': 2033,
                          #'4': 2034,
                          #'5': 2035,
                          }

# future and option maturity month code
FUTUREOPTION_MAT_MONTH = { '1': 1,
                           '2': 2,
                           '3': 3,
                           '4': 4,
                           '5': 5,
                           '6': 6,
                           '7': 7,
                           '8': 8,
                           '9': 9,
                           'A': 10,
                           'B': 11,
                           'C': 12,
                           }



def kisin(code) :
    """ 
    Korea ISIN code parser
    shortedn codes for future, option are allowed.
    """
    N = len(code)
    if N == 12:
        return kisin_longcode(code)
    elif N == 8 :
        return kisin_longcode(kisin_shortoption_8(code))
    elif N == 5 :
        return kisin_longcode(kisin_shortfuture_5(code))
    else:
        raise ValueError("unknown code lenght.")
        

def kisin_longcode(code):
    """
    Standard (long) code for Korea ISIN system
    """
    codedic = {}
    codedic['code'] = code
    
    # length
    if len(code) != 12:
        raise ValueError("code length must be 12.")
    
    # checksum
    code2 = calc_checkdigit(code)
    if code2 != code:
        raise ValueError("checksum is wrong. " + code2 + "is correct.")
    
    # country
    if code[0:2] != 'KR':
        raise ValueError("Not korea ISIN.")
    
    #category 
    if CATEGORY.has_key(code[2]):
        codedic['category'] = CATEGORY[code[2]]
        
        # future and option
        if codedic['category'] == CATEGORY['4']:
        
            if FUTUREOPTION_CATEGORY.has_key(code[3]):
                codedic['type'] = FUTUREOPTION_CATEGORY[code[3]]
            else:
                raise ValueError("unknown future & option sub-category.")
            # underlying
            if FUTUREOPTION_UNDERLYING.has_key(code[4:6]) :
                codedic['underlying'] = FUTUREOPTION_UNDERLYING[code[4:6]]
            else:
                raise ValueError("unknown underlying.")
            # mat year 
            if FUTUREOPTION_MAT_YEAR.has_key(code[6]) :
                codedic['matyear'] = FUTUREOPTION_MAT_YEAR[code[6]]
            else:
                raise ValueError("unknown maturity year.")
            # mat month 
            if FUTUREOPTION_MAT_MONTH.has_key(code[7]) :
                codedic['matmonth'] = FUTUREOPTION_MAT_MONTH[code[7]]
            else:
                raise ValueError("unknown maturity month.")
            # strike
            if ['call', 'put'].count(codedic['type']):
                strike = futureoption_strike(code[8:11])
                codedic['strike'] = strike
    else:
        raise ValueError("unknown category.")
        
    return codedic
         

def kisin_shortfuture_5(code):
    """
    convert a 5-digit shortened future code into a standard code
    """
    if len(code) != 5:
        raise ValueError("Not short future code. length must be 5.")
    if code[:3] != '101':
        raise ValueError("Not short future code. not start with 101.")
    if FUTUREOPTION_MAT_YEAR.has_key(code[3]) != 1 or \
       FUTUREOPTION_MAT_MONTH.has_key(code[4]) != 1 :
        raise ValueError("Not short future code. unknown year and month.")
    return calc_checkdigit('KR4101' + code[3:5] + '000' + 'X')


def kisin_shortoption_8(code):
    """
    convert a 8-digit shortened option code into a standard code
    """
    if len(code) != 8:
        raise ValueError("Not short option code. length must be 5.")
    if code[:3] != '201' and code[:3] != '301' :
        raise ValueError("Not short option code. not start with 101.")
    if FUTUREOPTION_MAT_YEAR.has_key(code[3]) != 1 or \
       FUTUREOPTION_MAT_MONTH.has_key(code[4]) != 1 :
        raise ValueError("Not short option code. unknown year and month.")
    flag, strike = futureoption_strike(code[5:8])
    if flag != 'option':
        raise ValueError("Not short option code. unknown strike.")
    return calc_checkdigit('KR4' + code + 'X')


def calc_checkdigit(code):
    """
    Calculate and return the check digit
    this code is copied from http://code.activestate.com/recipes/498277/ 
    """
    #Convert alpha characters to digits
    isin2 = []
    for char in code[:-1]:
        if char.isalpha():
            isin2.append((ascii_uppercase.index(char.upper()) + 9 + 1))
        else:
            isin2.append(char)
    #Convert each int into string and join
    isin2 = ''.join([str(i) for i in isin2])
    #Gather every second digit (even)
    even = isin2[::2]
    #Gather the other digits (odd)
    odd = isin2[1::2]
    #If len(isin2) is odd, multiply evens by 2, else multiply odds by 2
    if len(isin2) % 2 > 0:
        even = ''.join([str(int(i)*2) for i in list(even)])
    else:
        odd = ''.join([str(int(i)*2) for i in list(odd)])
    even_sum = sum([int(i) for i in even])
    #then add each single int in both odd and even
    odd_sum = sum([int(i) for i in odd])
    mod = (even_sum + odd_sum) % 10
    listcode = list(code)
    listcode[-1] = (str(10 - mod))[-1]
    code = str("".join(listcode))
    return code

def futureoption_strike(skrikecode):
    """
    validate and calculate strike for future and option
    """
    if skrikecode == '000':
        return None
    elif skrikecode.isdigit() and ['0','2','5','7'].count(skrikecode[2]) == 1:
        return round(int(skrikecode)/2.5)*2.5
    else:
        raise ValueError(skrikecode +" is unknown future option strike code.")
    


if __name__ == "__main__":
    kisin('KR4201DC1870')
